#region license
// Copyright (c) 2005, Sorin Ionescu (sorin.ionescu@gmail.com)
// All rights reserved.
// 
// Redistribution and use in source and binary forms, with or without modification,
// are permitted provided that the following conditions are met:
// 
//     * Redistributions of source code must retain the above copyright notice,
//     this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above copyright notice,
//     this list of conditions and the following disclaimer in the documentation
//     and/or other materials provided with the distribution.
//     * Neither the name of Sorin Ionescu nor the names of its
//     contributors may be used to endorse or promote products derived from this
//     software without specific prior written permission.
// 
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

# Taken and modified from the original code.

# This file is part of Grawlix.
# 
# Grawlix is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of
# the License, or (at your option) any later version.
# 
# Grawlix is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public
# License along with Grawlix.  If not, see
# <http://www.gnu.org/licenses/>.
#endregion

"""A compiler step to handle correct dispsoable overriding."""

namespace Grawlix.Meta
import Boo.Lang.Compiler
import Boo.Lang.Compiler.TypeSystem
import System

internal transient class ImplementDisposablePattern(
	Steps.AbstractNamespaceSensitiveVisitorCompilerStep):
"""Implement the disposable pattern on types with the proper attribute."""

	public override def Run():
	"""Start the step."""
		Visit(CompileUnit)
	
	public static def MatchParameters(
		[required] signature as (IType),
		[required] parameters as (IParameter)):
	"""
	Check whether a signature matches specified parameters.
	Param signature: The parameter types to match.
	Param parameters: The parameters to match against.
	"""
		if parameters.Length != signature.Length:
			return false
		for sigType as IType, param as IParameter in \
			zip(signature, parameters):
			if sigType != param.Type:
				return false
		return true
	
	public override def OnClassDefinition(node as Ast.ClassDefinition):
	"""
	Bookkeeping and ordering for class definitions.
	Param node: The class definition to process.
	"""
		MarkVisited(node)
		for baseTypeRef as Ast.TypeReference in node.BaseTypes:
			EnsureRelatedNodeWasVisited(node, GetType(baseTypeRef));
		
		ProcessClassDefinition(node)
		super(node)
		
	public def ProcessClassDefinition(node as Ast.ClassDefinition):
	"""
	Implement the pattern on class definitions.
	Param node: The class definition to resolve.
	"""
		# Get the information from the original attribute
		data = DisposableAttribute.FindNodeData(node)
		if data is null:
			return
		
		typeTag = TypeSystemServices.GetEntity(node) as IType
		disposableTag = TypeSystemServices.Map(System.IDisposable)
		
		baseIsDisposable = typeTag.BaseType.IsSubclassOf(disposableTag)
		baseDispose = FindMethod(typeTag.BaseType, 'Dispose')
		baseProtectedDispose = FindMethod(
			typeTag.BaseType,
			'Dispose',
			TypeSystemServices.BoolType)
		baseFinalizer = FindMethod(typeTag.BaseType, 'Finalize')
		# Should at least be in object
		assert baseFinalizer is not null
		if baseFinalizer.DeclaringType == TypeSystemServices.ObjectType:
			baseFinalizer = null
		
		# Fix incompatible schemes according to guidelines here:
		# < http://www.bluebytesoftware.com/blog/PermaLink.aspx? \
		# guid=88e62cdf-5919-4ac7-bc33-20c06ae539ae >
		
		if not baseIsDisposable and \
			baseDispose is null and \
			baseProtectedDispose is null and \
			baseFinalizer is null:
		# Correct implementation; new base type
			RemoveMethod(node, IsExplicitDisposeMethod)
			if not data.WantFinalizer:
				RemoveMethod(node, IsFinalizer)
		elif baseIsDisposable and \
			MethodIsCorrectlySealed(baseDispose) and \
			MethodIsCorrectlyVirtual(baseProtectedDispose):
		# Correct implementation; derived type
		# Assumes finalizer is correct
			node.BaseTypes.RemoveByEntity(disposableTag)
			RemoveMethod(node, IsExplicitDisposeMethod)
			RemoveMethod(node, IsDisposeMethod)
			OverrideMethod(
				node,
				IsProtectedDisposeMethod,
				baseProtectedDispose,
				InheritProtectedDispose(data, baseProtectedDispose),
				false)
			if baseFinalizer is not null or not data.WantFinalizer:
				RemoveMethod(node, IsFinalizer)
		else:
		# Faulty implementation
			if baseDispose is null:
			# No dispose present
			# create public sealed
				if baseIsDisposable:
					node.BaseTypes.RemoveByEntity(disposableTag)
				RemoveMethod(node, IsExplicitDisposeMethod)
			elif baseProtectedDispose is null:
			# Dispose present, but no protected dispose
			# Create private sealed
				if baseIsDisposable:
					node.BaseTypes.RemoveByEntity(disposableTag)
				RemoveMethod(node, IsDisposeMethod)
			else:
			# Dispose and protected dispose both present
			# If base is not sealed, override sealed
				if baseIsDisposable:
					node.BaseTypes.RemoveByEntity(disposableTag)
				RemoveMethod(node, IsExplicitDisposeMethod)
				if MethodIsExtensible(baseDispose):
					OverrideMethod(
						node,
						IsDisposeMethod,
						baseDispose,
						null,
						true)
			
			# Override or create a new method
			if MethodIsExtensible(baseProtectedDispose):
				OverrideMethod(
					node,
					IsProtectedDisposeMethod,
					baseProtectedDispose,
					InheritProtectedDispose(data, baseProtectedDispose),
					false)
			
			# Fix or create the finalizer
			if baseFinalizer is not null:
				if MethodIsExtensible(baseFinalizer):
					OverrideMethod(
						node,
						IsFinalizer,
						baseFinalizer,
						null,
						true)
			elif not data.WantFinalizer:
				RemoveMethod(node, IsFinalizer)
	
	public def FindMethod(
		[required] typeTag as IType,
		[required] methodName as string,
		[required] *signature as (IType)) as IMethod:
	"""
	Figure out some information about the method.
	Param typeTag: The type to look in.
	Param methodName: The name of the method to find.
	Param signature: The parameter types to match.
	"""
		savedNamespace = CurrentNamespace
		try:
			EnterNamespace(typeTag)
			candidates = \
				NameResolutionService.Resolve(
					typeTag,
					methodName,
					EntityType.Method);
			if candidates is null:
				return null
			
			if EntityType.Method == candidates.EntityType:
				method = candidates as IMethod
				if MatchParameters(signature, method.GetParameters()):
					return method
			
			if EntityType.Ambiguous == candidates.EntityType:
				entities = (candidates as Ambiguous).Entities
				for iter in entities:
					candidate = iter as IMethod
					if MatchParameters(signature, candidate.GetParameters()):
						return candidate
			return null
		ensure:
			NameResolutionService.Restore(savedNamespace)
	
	static def MethodIsCorrectlySealed(method as IMethod):
	"""
	Checks whether a method is sealed well.
	Returns: true if sealed well, else false.
	"""
		return method is not null and \
			method.IsPublic and \
			not method.IsVirtual and \
			not method.IsStatic
		
	static def MethodIsCorrectlyVirtual(method as IMethod):
	"""
	Checks whether a method is okay to override.
	Returns: true if good, else false.
	"""
		return method is not null and \
			method.IsProtected and \
			method.IsVirtual and \
			not method.IsAbstract and \
			not method.IsStatic
		
	static def MethodIsExtensible(method as IMethod):
	"""
	Checks whether a method can be overridden.
	Returns: true if good, else false.
	"""
		return method is not null and \
			(method.IsProtected or method.IsPublic) and \
			method.IsVirtual and \
			not method.IsStatic
	
	static def RemoveMethod(
		[required] node as Ast.TypeDefinition,
		[required] predicate as callable(Ast.Method) as bool):
	"""
	Remove the public final dispose method.
	Param node: The class to remove it from.
	Param predicate: The test function to use.
	"""
		members = node.Members
		node.Members = Ast.TypeMemberCollection(node)
		for member in members:
			if member.NodeType == Ast.NodeType.Method:
				method = member as Ast.Method
				if predicate(method):
					continue
			node.Members.Add(member)
	
	protected static def OverrideMethod(
		[required] node as Ast.TypeDefinition,
		[required] predicate as callable(Ast.Method) as bool,
		[required] base as IMethod,
		apply as callable(Ast.Method),
		sealed as bool):
	"""
	Adjusts the protected dispose method if it needs to override.
	Param node: The owning class to alter.
	Param predicate: The test function to use.
	Param base: The base method.
	Param apply: If not null, apply this function
		to the method after overriding.
	Param sealed: true if the final modifier should be added.
	"""
		for member in node.Members:
			if member.NodeType == Ast.NodeType.Method:
				method = member as Ast.Method
				if predicate(method):
					method.Modifiers = Ast.TypeMemberModifiers.Override
					if base.IsPublic:
						method.Modifiers |= Ast.TypeMemberModifiers.Public
					else:
						method.Modifiers |= Ast.TypeMemberModifiers.Protected
					if sealed:
						method.Modifiers |= Ast.TypeMemberModifiers.Final
					if apply is not null:
						apply(method)
	
	public static def InheritProtectedDispose(
		[required] data as DisposableData,
		[required] base as IMethod):
	"""
	Get a closure that should be applied to every disposing override.
	Param definition: The definition the method will be in.
	Param base: The method to derive from.
	Returns: The closure to apply.
	"""
		return do (derived as Ast.Method):
			paramName = base.GetParameters()[0].Name
			paramRef = Ast.ReferenceExpression(
				data.LexicalInfo,
				paramName)
			superInvocation = [|
				super($paramRef)
			|]
			derived.Parameters[0].Name = paramName
			superInvocation.LexicalInfo = data.LexicalInfo
			assert derived.Body.Statements.Count == 4
			addPoint = derived.Body.Statements[2] as Ast.Block
			assert addPoint is not null
			addPoint.Add(superInvocation)
	
	static def IsDisposeMethod([required] method as Ast.Method):
	"""
	Test whether a method is a public sealed dispose method.
	Param method: The method to test.
	Returns: true upon match, false otherwise.
	"""
		return method.Name == 'Dispose' and \
			method.Parameters.Count == 0 and \
			method.ExplicitInfo is null
	
	static def IsExplicitDisposeMethod([required] method as Ast.Method):
	"""
	Test whether a method is an explicit dispose method.
	Param method: The method to test.
	Returns: true upon match, false otherwise.
	"""
		return method.Name == 'Dispose' and \
			method.Parameters.Count == 0 and \
			method.ExplicitInfo is not null
	
	def IsProtectedDisposeMethod([required] method as Ast.Method):
	"""
	Test whether a method is a protected virtual dispose method.
	Param method: The method to test.
	Returns: true upon match, false otherwise.
	"""
		if method.Name != 'Dispose' or \
			method.Parameters.Count != 1 or \
			method.ExplicitInfo is not null:
			return false
		type = TypeSystemServices.GetEntity(method.Parameters[0].Type) as IType
		assert type is not null
		return type == TypeSystemServices.BoolType
	
	static def IsFinalizer([required] method as Ast.Method):
	"""
	Test whether a method is a finalizer.
	Param method: The method to test.
	Returns: true upon match, false otherwise.
	"""
		return method.Name == 'Finalize'
